
import curl, pycurl
import re
import time

from ezPyCrypto import key
import yaml

import pygtk
pygtk.require('2.0')
import gtk # sets app name
import gnomekeyring
import pickle

import ui

import os 
import sys

import log as lg

KEYRING_NAME = "balancer.credentials"

class BalanceFilter:
    """Filter the a balance to contain less information."""
    
    def __init__(self):
        self.__filters = {}


    def add_filter(self, source_uuid, account_uuid, hide=False, rename=None):
        """Add an account filter. If "hide" is true, then the account won't be shown. 
            if "rename" is non-None, then the name will be set to the given value."""

        if not self.__filters.has_key(source_uuid):
            self.__filters[source_uuid] = {}

        self.__filters[source_uuid][account_uuid] = {
            "hide" : hide,
            "rename" : rename
        }


    def should_remove(self, source_uuid, account_uuid):
        """Return true if the account should be filtered out, false otherwise."""
        try:
            return self.__filters[source_uuid][account_uuid]['hide']
        except KeyError:
            return False

    def filtered_name(self, source_uuid, account_uuid, name):
        """Return the filtered name of the account. If the name shouldn't be filtered, 
            returns the default passed in the arguments."""
        try:
            rename = self.__filters[source_uuid][account_uuid]['rename']
            if None != rename:
                return rename
        except KeyError:
            pass

        return name

    def filter(self, results):
        """Creates a new result object that has been fully filtered."""
        accts = []

        for oldAcct in results.get_accounts():
            src_uuid = oldAcct.get_source().get_source_uuid()
            uuid = oldAcct.get_uuid()
            if self.should_remove(src_uuid, uuid):
                continue

            name = self.filtered_name(src_uuid, uuid, oldAcct.get_name())

            acct = BalanceAccount(
                    oldAcct.get_source(), 
                    type=oldAcct.get_type(), 
                    moniker=oldAcct.get_moniker(), 
                    uuid=oldAcct.get_uuid(), 
                    balance=oldAcct.get_balance(), 
                    name=name)

            accts.append(acct)

        return Balance(accts)
        

    def to_save_structure(self):
        return self.__filters

    def become_save_structure(self, structure):
        f = BalanceFilter()
        f.__filters = structure

        return f


class BalanceSource:
    """Superclass for things that get balances."""

    FAST = "fast"

    def load(self, credential_store, progress, rate=None, log=None):
        """Load the balance. Returns account info. """
        pass

    def get_name(self):
        """Return the human meaningful name of the source."""

    def pause(self, rate, seconds, progress):
        """Tell the source to pause briefly during a load."""
        if BalanceSource.FAST == rate:
            time.sleep(seconds / 10)
            if progress:
                progress.worked(None, seconds)
        else:
            for i in range(seconds):
                time.sleep(1)
                if progress:
                    progress.worked(None, 1)


class Balance:
    """Result of a balance request."""
    
    def __init__(self, accounts=None):
        if accounts == None:
            accounts = []
        self.accounts = accounts

    def show(self):
        for acct in self.accounts:
            print "Acct: %s  %s   %s   %s" % (acct.get_name(), acct.get_moniker(), acct.get_type(), acct.get_balance())

    def get_accounts(self):
        return self.accounts

    def devour(self, otherBalance):
        self.accounts.extend(otherBalance.get_accounts())

class BalanceAccount:
    """The information describing an account."""
    CHEQUING="CHEQUING"
    CREDIT_CARD="CREDIT_CARD"

    def __init__(self, source, type=None, moniker=None, uuid=None, balance=None, name=None):
        self.source = source
        self.type = type
        self.moniker = moniker
        self.uuid = uuid
        self.balance = balance
        self.name = name

    def get_name(self):
        """Get the human-readable name, or None, if one doesn't exist."""
        return self.name

    def get_balance(self):
        """Get the current balance."""
        return self.balance

    def get_type(self):
        """Get the type of account."""
        return self.type

    def get_moniker(self):
        """Get the institution's nickname for the account, or None, if that is not appropriate."""
        return self.moniker

    def get_uuid(self):
        """Get a globally unique ID for this account, that can be used to store information between invocations."""
        return self.uuid

    def get_source(self):
        """Return the balance source"""
        return self.source

    def __str__(self):
        return "Name: %s  Moniker: %s  Balance: %s" % (self.name, self.moniker, self.balance)


class BalancerException(Exception):
    """Base class for all exceptions thrown in balancer.""" 

    def __init__(self, error):
        self.__error = error

    def __str__(self):
        return self.__error

    def get_message(self):
        return self.__error

class BalancerLoadException(BalancerException):
    """Indicates that a loader failed somewhere."""

    def __init__(self, error):
        super(BalancerLoadException, self).__init__(error)


class BalancerLoginException(BalancerLoadException):
    """Indicates that the credentials given to the loader didn't validate."""

    def __init__(self, message):
        super(BalancerLoginException, self).__init__(message)


class ScotiaBankBalanceSource(BalanceSource):
    """Gets balance from ScotiaBank online"""

    def get_name(self):
        return "Bank of Nova Scotia"

    def __make_log(self, log):
        if log == None:
            return lg.NullLog()
        return log

    def load(self, cred, progress, rate=None, log=None):
        log = self.__make_log(log)
        grp = log.make_family()

        BASE = "https://www.scotiaonline.scotiabank.com"
        c = curl.Curl()
        #c.set_option(pycurl.VERBOSE, 1)
        c.set_option(pycurl.TIMEOUT, 60)
        c.set_option(pycurl.USERAGENT, "User-Agent=Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.12) Gecko/2009070811 Ubuntu/9.04 (jaunty) Firefox/3.0.12")

        progress.set_work_remaining(40)
        progress.worked("Connecting", 0)

        # Acquire our session cookie. 
        url = BASE + "/online/start.jsp?language=&first=true"
        log.log(grp, lg.START_LOAD, "Loading (1) from " + url)
        next = c.get(url)
        self.__ensure("REFRESH.*/online/start.jsp", next, "Missing login page")

        # We've got the cookie. Get the login page.
        self.pause(rate, 7, progress)
        progress.worked("Getting login page", 3)
        url = BASE + "/online/start.jsp?language=&first=true"
        log.log(grp, lg.START_LOAD, "Loading (2)")
        loginPage = c.get(url)
        loginVars = self.__extract_loginFields(loginPage)
        
        progress.worked("Submitting login page", 5)

        (attr, secrets, name) = cred.load(progress.new_child(5))
        next = c.post(BASE + "/online/start.jsp", {
            loginVars[0] : secrets[0],
            loginVars[1] : secrets[1],
            'LOGIN_MARKER' : loginVars[2],
        })
        if not re.search(r"Refresh.*/portal/portal_frontDoor.jsp", next):
            log.log(grp, lg.CREDENTIAL_FAIL, "Credential failure for %s" % secrets[0])
            raise BalancerLoginException("Login failed for card number %s" % secrets[0])

        # Follow the redirect
        progress.worked("Getting base page", 10)
        next = c.get(BASE + "/portal/portal_frontDoor.jsp")

        # Hit the home page
        self.pause(rate, 4, progress)
        progress.worked("Getting base page", 6)
        next = c.get(BASE + "/portal/index.jsp?")

        r = self.__extract_accounts(next)
        log.log(grp, lg.SUCCESS)
        return r


    def __ensure(self, regex, page, error):
        """Throw an exception if the given regex isn't present."""
        if not re.search(regex, page, re.DOTALL):
            raise BalancerLoadException(error)


    def __ensure_match(self, match, error):
        if not match:
            raise BalancerLoadException(error)


    def __extract_loginFields(self, loginPage):
        """Find the name of the silly login fields.
            Return a goofy array: [ScotiaCardFieldName, PasswordFieldName, LoginMarkerValue]
        """

        # Find the ScotiaCard field name
        match = re.search(r"<input type=\"text\" value=\"453\" name=\"(\d+?)\"", loginPage)
        self.__ensure_match(match, "Could not find card entry field")
        cardName = match.group(1)

        # Find the password field name
        match = re.search(r"<input type=\"password\" name=\"(\d+?)\"", loginPage)
        self.__ensure_match(match, "Could not find password field")
        pwName = match.group(1)

        # Find the Login marker
        match = re.search(r"<input type=\"hidden\" name=\"LOGIN_MARKER\" value=\"(\d+?)\"", loginPage)
        self.__ensure_match(match, "Could not find marker")
        marker = match.group(1)
    
        return [cardName, pwName, marker]


    def __extract_accounts(self, page):
        """
           Extract account info from the user's base page. 
        """
        accts = []
        # Get account summary table
        regex = r"<tr class='bgcoloroption[12]'>.*?<td class=\"tableDataLeftC\">\s*([^<]+)\s*</td>.*?<td[^>]*?><a[^>]*?>([^<]+?)</a></td>.*?<td[^>]*?>([^<]*?)</td>.*?</tr>"
        self.__ensure(regex, page, "Could not find any accounts")

        for m in re.finditer(regex, page, re.DOTALL):
            name = m.group(1).strip()
            moniker = m.group(2).strip()
            balance = self.__dehumanize(m.group(3).strip())
            print "Name: %s  Moniker: %s  Balance: %s" % (name, moniker, balance)

            type = self.__make_type(moniker)
            uuid = self.__make_uuid(moniker)

            acct = BalanceAccount(self, type=type, moniker=moniker, uuid=uuid, balance=balance, name=name)
            accts.append(acct)

        return Balance(accts)

    def __dehumanize(self, str):
        """Turn a human-meaningful number into a number number."""
        return re.sub(r"[^-$.01-9]", '', str)

    def __make_type(self, str):
        if str.startswith('***'):
            return BalanceAccount.CHEQUING

        return BalanceAccount.CREDIT_CARD

    def __make_uuid(self, str):
        return "ScotiaBank_" + str


    def get_source_uuid(self):
        return "ScotiaBank"

class CredentialStore:
    """A class that stores credentials on behalf of source."""

    def __init__(self, source, phrase, key_string=None):
        """Create a store for the given source, with the given passphrase"""
        if key_string == None:
            self.__key = key(1024, passphrase=phrase)
        else:
            self.__key = key()
            self.__key.importKey(key_string, passphrase=phrase)


    def get_my_credetials_plaintext(self):
        """Return the plaintext of the credentials stored for the source."""
        cred = []
        for c in self.__credentials:
            cred.append(self.__key.decString(c))

        return cred


    def get_my_credetials_obfuscated(self):
        """Return the obfuscated credentials stored for the source."""
        return self.__credentials


    def set_my_credentials_plaintext(self, list_of_credentials):
        """Set the plaintext credentials stored for the source."""        
        self.__credentials = []

        for c in list_of_credentials:
            self.__credentials.append(self.__key.encString(c))

    def set_my_credentials_obfuscated(self, list_of_credentials):
        """Set the obfuscated credentials stored for the source."""        
        self.__credentials = list_of_credentials


    def get_key(self):
        """Return a stringified key."""
        return self.__key.exportKeyPrivate()


class Progress:
    """A simple progress tracker."""

    def __init__(self):
        self.__total = 0
        self.__worked = 0

    def set_work_remaining(self, count):
        self.__total += count

    def worked(self, message, count):
        self.__worked += count
        print "Progress: %s (%s/%s)" % (message, self.__worked, self.__total)

    def finished(self):
        pass

    def new_child(self, units):
        return Progress()

class CredentialStore2:
    """A credential store."""

    def __init__(self, id=None):
        self.__secrets = []
        self.__id = id

    def __validate(self, progress):
        item = gnomekeyring.item_get_info_sync(KEYRING_NAME, self.__id)
        attr = gnomekeyring.item_get_attributes_sync(KEYRING_NAME, self.__id)
        name = item.get_display_name()
        

    def save(self, name, attrs, secrets, progress):
        """Set the secrets for this store. The 'secrets' is a list of one or more strings."""
        # Make sure we have a keyring.
        try:
            gnomekeyring.create_sync(KEYRING_NAME, None)
        except gnomekeyring.AlreadyExistsError:
            pass

        # Save the secret
        self.__id = gnomekeyring.item_create_sync(KEYRING_NAME, gnomekeyring.ITEM_GENERIC_SECRET, name, attrs, pickle.dumps(secrets), True)

        # Update the ACL
        ac = gnomekeyring.AccessControl(gnomekeyring.ApplicationRef(), 
                gnomekeyring.ACCESS_READ | gnomekeyring.ACCESS_WRITE | gnomekeyring.ACCESS_REMOVE)
        ac.set_display_name(os.path.basename(sys.argv[0]))
        ac.set_path_name(os.path.realpath(sys.executable))
        gnomekeyring.item_set_acl_sync(KEYRING_NAME, self.__id, [ac])


    def load(self, progress):
        """Return the secrets and attributes. The attributes are returned 
            first in a hash, then the secrets are returned as a list."""
        item = gnomekeyring.item_get_info_sync(KEYRING_NAME, self.__id)

        attr = gnomekeyring.item_get_attributes_sync(KEYRING_NAME, self.__id)
        secret = pickle.loads(item.get_secret())
        name = item.get_display_name()

        return (attr, secret, name)


def credentials(progress):
    """Get all of the known credentials."""
    creds = []
    for id in gnomekeyring.list_item_ids_sync(KEYRING_NAME):
        # Create a new credentialstore
        store = CredentialStore2(id)
        creds.append(store)

    return creds


def has_keyring():
    """Check to see if we have a keyring. If we do, it can be unlocked. If we don't, we should create one."""
    return KEYRING_NAME in gnomekeyring.list_keyring_names_sync()


def unlock(passphrase):
    """Unlock our keyring."""
    r = gnomekeyring.unlock_sync(KEYRING_NAME, passphrase)


def create_keyring():
    """Create a keyring with the given passphrase."""
    try:
        gnomekeyring.create_sync(KEYRING_NAME, None)
    except gnomekeyring.AlreadyExistsError:
        pass


def reset():
    """Reset the credential store. Return True if the keyring was deleted"""
    try:
        gnomekeyring.delete_sync(KEYRING_NAME)
    except gnomekeyring.NoSuchKeyringError:
        print "No such keyring"
